﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection;
using CLIFileRW;
using System.Diagnostics;

namespace ILMatcher
{
    public class ILFragment //fixme: magari mettere direttamente l'array se non servono altre informazioni
    {
        //fixme: aggiungere anche l'instruzione di load a cui si fa il bind?
        //fixme: aggiungere begine e end all'equal? lasciarli o non serve?
        public long begin;
        public long end;

        public ILInstruction[] fragment;

        public ILFragment(ILInstruction[] fragment, long begin, long end)
        {
            this.fragment = fragment;
            this.begin = begin;
            this.end = end;
        }

        public ILFragment(ILInstruction[] fragment, long beginEnd) : this(fragment, beginEnd, beginEnd) { } 

        public override int GetHashCode()
        {
            return base.GetHashCode();
        }

        public override bool Equals(object obj)
        {
            if (!obj.GetType().Equals(typeof(ILFragment))) return false;
            ILFragment cf1 = (ILFragment)obj;
            if (cf1.fragment.Length != fragment.Length) return false;
            for (int i = 0; i < fragment.Length; i++)
            {
                if (!fragment[i].Equals(cf1.fragment[i])) return false;
            }
            return true;
        }

      
    }

    public struct Match
    {
        public long initialPosition;
        public long finalPosition;
        public ILFragment[] bindings;

        public Match(long initialPosition, long finalPosition, ILFragment[] bindings)
        {
            this.initialPosition = initialPosition;
            this.finalPosition = finalPosition;
            this.bindings = bindings;
        }
    }

    //todo: eliminarla ed usare solo una struttura, possibilmente evaluationstep
    public struct CodeSection //fixme farla tornare struct, forse posso anche toglierla del tutto e usare un semplice keyvaluepair
    {
        private int automatonPosition;
        private long initialPosition;
        private long finalPosition;
        private ILFragment[] bindings;

        //todo: controllare l'uso di stacksize, secondo me si può togliere
        public CodeSection(int automatonPos, long initialPosition, long finalPosition, int nArgs)
        {
            this.automatonPosition = automatonPos;
            this.initialPosition = initialPosition;
            this.finalPosition = finalPosition;
            bindings = new ILFragment[nArgs];
        }

        public CodeSection(int automatonPos, long initialPosition, long finalPosition, ILFragment[] fragments)
        {
            this.automatonPosition = automatonPos;
            this.initialPosition = initialPosition;
            this.finalPosition = finalPosition;
            bindings = fragments;
        }

        public override string ToString()
        {
            return "(" + initialPosition + ", " + finalPosition + ")";
        }

        public long InitialPosition
        {
            get { return initialPosition; }
        }

        public long FinalPosition
        {
            get { return finalPosition; }
        }

        public int AutomatonPosition
        {
            get { return automatonPosition; }
        }

        public ILFragment[] Bindings
        {
            get { return bindings; }
        }
    }

    public class Query
    {
        private MethodInfo methodQuery;
        private List<MatchNode> nodes;
        private List<Match> matchesFound; //todo: farla diventare array alla fine
        private CLIType[] args;
        private CLIType[] vars;
        private CLIType[] qargs;
        private bool isCompiled;
        private CodeSection currentEvaluationStep;

        public Query(MethodInfo m)
        {
            if (null == m) throw new ArgumentNullException("m");
            methodQuery = m;
        }

        #region Methods

        public static void QueryBegin(){}

        public void Compile()
        {
            //todo: completare il set di istruzioni possibile
            nodes = new List<MatchNode>();

            MethodTableCursor cur = CLIFile.FindMethod(methodQuery);
            CLIFileRW.MethodBody cb = cur.MethodBody;
            LocalsVarSig locals = cb.LocalsSignature;

            CLIType[] vars = cb.LocalsSig > 0 ? locals.GetVariables().ToArray<CLIType>() : null;
            CLIType[] args = cur.GetMethodSignature().GetParameters(cur.FindCLIType()).ToArray<CLIType>();

            qargs = args;

            ILCursor ilc = cb.ILInstructions;
            ilc.TrackStack(args, vars, cb.MaxStack);
            ilc.TrackTargets();

            bool[] varIsDelegate = vars != null ? new bool[vars.Length] : null;
            bool[] argIsDelegate = args != null ? new bool[args.Length] : null;

            //check if args or locals variables are delegates and set to true in support bool arrays
            if(null != vars)
                for (int i = 0; i < vars.Length; i++)
                    varIsDelegate[i] = vars[i].GetReflectionType().IsSubclassOf(typeof(Delegate));
            if(null != args)
                for (int i = 0; i < args.Length; i++)
                    argIsDelegate[i] = args[i].GetReflectionType().IsSubclassOf(typeof(Delegate));

            int pos = -1; //position into args/vars arrays

            while (ilc.Next())
            {
                ILInstruction instr = ilc.Instr;
                OPCODES op = Query.getOpcode(instr); //(OPCODES)((instr.op.Value < 0 ? 256 : 0) + (instr.op.Value & 0xFF));

                switch (op)
                {
                    case OPCODES.Nop:
                        break;
                    case OPCODES.Ret:
                        break;
                    case OPCODES.Stloc:
                    case OPCODES.Stloc_0:
                    case OPCODES.Stloc_1:
                    case OPCODES.Stloc_2:
                    case OPCODES.Stloc_3:
                    case OPCODES.Stloc_S:
                        ILInstruction i = ILInstruction.Normalize(ilc.Instr);
                        nodes.Add(new StoreMatchNode(ilc, int.Parse(i.ResolveParameter(ilc.FileReader).ToString()), this));
                        break;
                    case OPCODES.Ldc_I4:
                    case OPCODES.Ldc_I4_0:
                    case OPCODES.Ldc_I4_1:
                    case OPCODES.Ldc_I4_2:
                    case OPCODES.Ldc_I4_3:
                    case OPCODES.Ldc_I4_4:
                    case OPCODES.Ldc_I4_5:
                    case OPCODES.Ldc_I4_6:
                    case OPCODES.Ldc_I4_7:
                    case OPCODES.Ldc_I4_8:
                    case OPCODES.Ldc_I4_S:
                    case OPCODES.Ldc_I4_M1:
                        nodes.Add(new LoadConstMatchNode(ilc, typeof(Int32), this));
                        break;
                    case OPCODES.Ldc_I8:
                        nodes.Add(new LoadConstMatchNode(ilc, typeof(Int64), this));
                        break;
                    case OPCODES.Ldc_R4:
                        nodes.Add(new LoadConstMatchNode(ilc, typeof(Single), this));
                        break;
                    case OPCODES.Ldc_R8:
                        nodes.Add(new LoadConstMatchNode(ilc, typeof(Double), this));
                        break;
                    case OPCODES.Ldarg:
                    case OPCODES.Ldarg_1:
                    case OPCODES.Ldarg_2:
                    case OPCODES.Ldarg_3:
                    case OPCODES.Ldarg_S:
                        MatchNode newMatchNode;
                        pos =  int.Parse(ILInstruction.Normalize(ilc.Instr).ResolveParameter(ilc.FileReader).ToString());
                        if (argIsDelegate[pos])
                        {
                            Type returnedType = ((CLIType)args[pos]).GetReflectionType().GetMethod("Invoke").ReturnType;
                            if (returnedType == typeof(void))
                            { //add KleeneStarNode
                                newMatchNode = new KleeneMatchNode(ilc, this);
                            }
                            else
                            {//add expMatchNode
                                newMatchNode = new ExpressionMatchNode(ilc, returnedType, this);
                            }
                        }
                        else
                        {
                            newMatchNode = new LoadMatchNode(ilc, args[pos].GetReflectionType(), this);
                            //fixme vedere come gestire il binding!
                        }
                        newMatchNode.BindWith = pos;
                        nodes.Add(newMatchNode);
                        break;
                    case OPCODES.Ldstr:
                        nodes.Add(new LoadMatchNode(ilc, typeof(string), this));
                        break;
                    case OPCODES.Call:
                        if (((MethodDesc)ilc.Instr.ResolveParameter(ilc.FileReader)).Name.Equals("QueryBegin"))
                        {//startPoint for query automata
                            //clearCurrent automata (it doesn't matter since it's only for declaring locals)
                            nodes.Clear();
                        }
                        else
                        {
                            nodes.Add(new CallMatchNode(ilc, this));
                        }
                        break;
                    case OPCODES.Calli:
                        throw new NotImplementedException();
                        break;
                    case OPCODES.Callvirt:
                        nodes.Add(new CallMatchNode(ilc, this));
                        break;
                    case OPCODES.Ldloc:
                    case OPCODES.Ldloc_0:
                    case OPCODES.Ldloc_1:
                    case OPCODES.Ldloc_2:
                    case OPCODES.Ldloc_3:
                    case OPCODES.Ldloc_S:
                        pos = int.Parse(ILInstruction.Normalize(ilc.Instr).ResolveParameter(ilc.FileReader).ToString());
                        if (varIsDelegate[pos])
                        {
                            Type returnedType = vars[pos].GetReflectionType().GetMethod("Invoke").ReturnType;
                            if (returnedType == typeof(void)) //add KleeneStarNode
                                nodes.Add(new KleeneMatchNode(ilc, this));
                            else //add expMatchNode
                                nodes.Add(new ExpressionMatchNode(ilc, returnedType, this));
                        }
                        else
                            nodes.Add(new LoadMatchNode(ilc, vars[pos].GetReflectionType(), this));

                        break;
                    case OPCODES.Ldloca:
                        throw new NotImplementedException();
                        break;
                    case OPCODES.Ldloca_S:
                        throw new NotImplementedException();
                        break;
                   // case OPCODES.Newobj: //vedere se si può fare con il callNormale o se bisogna inserire una classe apposta
                        //((MethodDesc)ilc.Instr.ResolveParameter(ilc.FileReader)).GetReflectionMethod(null).DeclaringType
                     //   throw new NotImplementedException();
                      //  break;
                    default:
                        nodes.Add(new ExactMatchNode(ilc, this));
                        break;
                }
            }
            IsCompiled = true;
        }

        public bool Match(MethodInfo m)
        {
            if (!IsCompiled) throw new InvalidOperationException("Queries must be compiled before start matching");

            MethodTableCursor cur = CLIFile.FindMethod(m);
            CLIFileRW.MethodBody cb = cur.MethodBody;
            LocalsVarSig locals = cb.LocalsSignature;

            vars = locals.GetVariables().ToArray<CLIType>();
            args = cur.GetMethodSignature().GetParameters(cur.FindCLIType()).ToArray<CLIType>();

            ILCursor ilc = cb.ILInstructions;
            ilc.TrackStack(args, vars, cb.MaxStack);
            ilc.TrackTargets();

            //int currentNode = 0;

            Stack<CodeSection> evaluationStack = new Stack<CodeSection>();
            evaluationStack.Push(new CodeSection(0, ilc.Position, ilc.Position, new ILFragment[qargs.Length]));
            ilc.Next();
            ilc.PushState();
            
            //this one is to continue the conputation
            ilc.PushState();

            matchesFound = new List<Match>(); //fixme fare in modo che restituisca risultati utilizzabili :P

            bool exit = false;
            do
            {
                if (evaluationStack.Count > 0)
                {
                    currentEvaluationStep = evaluationStack.Pop();
                    ilc.PopState();
                    if (currentEvaluationStep.AutomatonPosition == nodes.Count)
                    {
                        //addMatch!
                        matchesFound.Add(new Match(currentEvaluationStep.InitialPosition, 
                                                   currentEvaluationStep.FinalPosition, 
                                                   currentEvaluationStep.Bindings));
                    }
                    else
                    {
                        while (ilc.Position < currentEvaluationStep.FinalPosition)
                            ilc.Next();
                        //if you find an automata node with at least one match, add each match to the stack
                        if (nodes[currentEvaluationStep.AutomatonPosition].Match(ilc))
                        {
                            while (nodes[currentEvaluationStep.AutomatonPosition].PossibleMatches.Count > 0)
                            {
                                evaluationStack.Push(new CodeSection(1+currentEvaluationStep.AutomatonPosition,
                                                                     currentEvaluationStep.InitialPosition,
                                                                     nodes[currentEvaluationStep.AutomatonPosition].PossibleMatches[0].finalPosition,
                                                                     nodes[currentEvaluationStep.AutomatonPosition].PossibleMatches[0].bindings)); //fixme: possible bottleneck?
                                ilc.PushState();
                                nodes[currentEvaluationStep.AutomatonPosition].PossibleMatches.RemoveAt(0);
                            }
                        }
                    }
                }
                else
                {   //devo far avanzare il cursore o uscire fixme: cambiare la descrizione
                    ilc.PopState();
                    if (ilc.Next())
                    {
                        ilc.PushState();
                         evaluationStack.Push(new CodeSection(0, ilc.Position, ilc.Position, new ILFragment[qargs.Length]));
                        ilc.PushState();
                    }else
                        exit = true;
                }
            } while (!exit);

            return matchesFound.Count > 0;
        }

        public static OPCODES getOpcode(ILInstruction instr)
        {
            return (OPCODES)((instr.op.Value < 0 ? 256 : 0) + (instr.op.Value & 0xFF));
        }

        #endregion

        #region Properties

        internal CodeSection CurrentEvaluationStep
        {
            get { return currentEvaluationStep; }
        }

        public List<Match> Matches
        {
            get { return matchesFound; }
        }

        public MethodInfo MethodQuery
        {
            get { return methodQuery; }
        }

        internal CLIType[] Args { get { return args; } }

        internal CLIType[] QueryArgs { get { return qargs; } }

        public CLIType[] Vars
        {
            get { return vars; }
        }

        public bool IsCompiled
        {
            get { return isCompiled; }
            set { isCompiled = value; }
        }
        #endregion

    }

    public static class MyExtension
    {
        public static bool Match(this MethodInfo m, Query q)
        {
            if (!q.IsCompiled) q.Compile();
            return q.Match(m);
        }

        public static Query GetQuery(this MethodInfo m)
        {
            return new Query(m);
        }
    }

    public class QueryCode : System.Attribute { }  

}
